package be.gnx.log4jeclipse.perspective.model;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;

import org.apache.log4j.spi.LoggingEvent;

public class LoggingEvents {

  private File loggingEventsFile;

  private FileOutputStream loggingEventsFileOutputStream;

  private int numberOfLoggingEvents = 0;

  private static final int defaultMaxCachedLoggingEvents = 100;

  private int maxCachedLoggingEvents;

  private Hashtable<Integer, LoggingEvent> cachedLoggingEvents;

  private List<Integer> cachedLoggingEventIndexes;

  private static final int defaultTwoTimesPositionCache = 1000;

  private int twoTimesPositionCache;

  private int loggingEventPositionTwoPowerGapSize;

  private List<Long> loggingEventPosition;

  public LoggingEvents() {
    this(defaultMaxCachedLoggingEvents, defaultTwoTimesPositionCache);
  }

  public LoggingEvents(int maxCachedLoggingEvents, int twoTimesPositionCache) {
    this.maxCachedLoggingEvents = maxCachedLoggingEvents;
    this.twoTimesPositionCache = twoTimesPositionCache;
    try {
      loggingEventsFile = File.createTempFile("log4jeclipse", "log");
      loggingEventsFile.deleteOnExit();
      loggingEventsFileOutputStream = new FileOutputStream(loggingEventsFile);
    } catch (IOException ioException) {
      throw new RuntimeException(ioException);
    }
    cachedLoggingEvents = new Hashtable<Integer, LoggingEvent>(maxCachedLoggingEvents);
    cachedLoggingEventIndexes = new ArrayList<Integer>(maxCachedLoggingEvents);
    loggingEventPosition = new ArrayList<Long>(2 * twoTimesPositionCache);
    loggingEventPositionTwoPowerGapSize = 0;
  }

  public boolean add(LoggingEvent loggingEvent) {
    if (cachedLoggingEventIndexes.size() >= maxCachedLoggingEvents) {
      cachedLoggingEvents.remove(cachedLoggingEventIndexes.get(0));
      cachedLoggingEventIndexes.remove(0);
    }
    cachedLoggingEvents.put(numberOfLoggingEvents, loggingEvent);
    cachedLoggingEventIndexes.add(numberOfLoggingEvents);
    try {
      ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
      ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
      objectOutputStream.writeObject(loggingEvent);
      byte[] loggingEventbytes = byteArrayOutputStream.toByteArray();
      if ((loggingEventPosition.size() >= (2 * twoTimesPositionCache))
          && ((numberOfLoggingEvents & ((1 << loggingEventPositionTwoPowerGapSize) - 1)) == 0)) {
        List<Long> newLoggingEventPosition = new ArrayList<Long>(2 * twoTimesPositionCache);
        for (int index = 0; index < loggingEventPosition.size(); index = index + 2) {
          newLoggingEventPosition.add(loggingEventPosition.get(index));
        }
        loggingEventPosition = newLoggingEventPosition;
        loggingEventPositionTwoPowerGapSize++;
      }
      if ((loggingEventPositionTwoPowerGapSize == 0)
          || ((numberOfLoggingEvents & ((1 << loggingEventPositionTwoPowerGapSize) - 1)) == 0)) {
        loggingEventPosition.add(loggingEventsFileOutputStream.getChannel().position());
      }
      loggingEventsFileOutputStream.write(loggingEventbytes);
      loggingEventsFileOutputStream.flush();
    } catch (IOException ioException) {
      throw new RuntimeException(ioException);
    }
    numberOfLoggingEvents++;
    return true;
  }

  public void clear() {
    try {
      loggingEventsFileOutputStream.getChannel().truncate(0);
    } catch (IOException ioException) {
      throw new RuntimeException(ioException);
    }
    cachedLoggingEvents.clear();
    cachedLoggingEventIndexes.clear();
    loggingEventPosition.clear();
    numberOfLoggingEvents = 0;
  }

  public LoggingEvent get(int index) {
    if (cachedLoggingEventIndexes.contains(index)) {
      cachedLoggingEventIndexes.remove(cachedLoggingEventIndexes.indexOf(index));
      cachedLoggingEventIndexes.add(index);
      return cachedLoggingEvents.get(index);
    }
    if (cachedLoggingEventIndexes.size() >= maxCachedLoggingEvents) {
      cachedLoggingEvents.remove(cachedLoggingEventIndexes.get(0));
      cachedLoggingEventIndexes.remove(0);
    }
    LoggingEvent loggingEvent = read(index);
    cachedLoggingEvents.put(index, loggingEvent);
    cachedLoggingEventIndexes.add(index);
    return loggingEvent;
  }

  public int size() {
    return numberOfLoggingEvents;
  }

  public LoggingEvent read(int index) {
    int currentIndex = (index >> loggingEventPositionTwoPowerGapSize) << loggingEventPositionTwoPowerGapSize;
    long currentPosition = loggingEventPosition.get(index >> loggingEventPositionTwoPowerGapSize);
    LoggingEvent loggingEvent = null;
    try {
      FileInputStream loggingEventsFileInputStream = new FileInputStream(loggingEventsFile);
      loggingEventsFileInputStream.getChannel().position(currentPosition);
      while (currentIndex <= index) {
        loggingEvent = (LoggingEvent) new ObjectInputStream(loggingEventsFileInputStream).readObject();
        currentIndex++;
      }
      loggingEventsFileInputStream.close();
    } catch (IOException ioException) {
      throw new RuntimeException(ioException);
    } catch (ClassNotFoundException classNotFoundException) {
      throw new RuntimeException(classNotFoundException);
    }

    return loggingEvent;
  }

}
